package javasec.seed;

import java.io.File;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.lang.reflect.Method;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.URL;
import java.net.URLClassLoader;

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

import java.util.Enumeration;
import java.util.Date;
import java.util.ArrayList;

import java.rmi.server.ExportException;
import java.rmi.server.RMIClientSocketFactory;
import java.rmi.server.RMIServerSocketFactory;

import javasec.seed.data.SQLiteDataSeed;

import javax.rmi.ssl.SslRMIClientSocketFactory;
import javax.rmi.ssl.SslRMIServerSocketFactory;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.MessageDigest;

import sun.security.x509.X500Name;
import sun.security.x509.CertAndKeyGen;

public class SeedServer
{
	public static RMIClientSocketFactory csf = null;
	public static RMIServerSocketFactory ssf = null;
	public static Registry rmiRegistry = null;

	// important path variables
	public static File seedHome;
	public static String keyFilename = "seed-id.key";
	public static String trustedFilename = "seed-trusted.key";
	public static File keyFile = null;
	public static File trustedFile = null;
	public static String keyPassword = null;
	public static String bindAddress=null;
	public static DataSeedInterface dataSeed=null;
	
	public static ArrayList<Seed> seeds = new ArrayList<Seed>();
	public static String securityMode = "open"; // open or private

	public static void main(String args[])
	{
		if((args.length==0)||(args.length>2))
		{
			System.out.println("usage: java -jar SeedServer.jar <password> [<bind address>]");
		}
		
		keyPassword=args[0];
		if(args.length==2)bindAddress=args[1];
		
		try
		{
			setHome(".seed");
			System.out.println("SeedServer: using " + seedHome.getAbsolutePath() + " as home");

			if(bindAddress==null)detectBindAddress();
			else System.setProperty("java.rmi.server.hostname", bindAddress); 
			System.out.println("using "+bindAddress+" as the bind address");
			
			dataSeed=new SQLiteDataSeed(); // start up database
			prepareCrypto(); // get keys and set up socket factory

			rmiRegistry = LocateRegistry.createRegistry(41414, csf, ssf);

			rmiRegistry.bind("DataSeed", dataSeed);
			new ManagerSeed();
			loadSeeds();
		}
		catch(ExportException ee)
		{
			ee.printStackTrace();
			System.out.println("\n ERROR: looks like a seed server is already running");
		}
		catch(Exception e)
		{
			e.printStackTrace();
			System.exit(1);
		}
	}

	static boolean setHome(String home) // set the home for the seed
	{
		seedHome = new File(System.getProperty("user.home") + File.separator + home);

		if(seedHome.exists())
		{
			System.out.println("SeedServer: seed directory is present");
			if(!seedHome.isDirectory())
			{
				System.out.println("SeedServer: " + home + " exists, but is a file");
				return false;
			}
		}
		else
		{
			System.out.println("SeedServer: detected first run, setting up in ~/" + home);
			seedHome.mkdir();
		}

		System.setProperty("user.dir", seedHome.getAbsolutePath());
		return true;
	}

	static boolean prepareCrypto()
	{
		KeyStore key = null;
		KeyStore trusted = null;
		Certificate keyCertificate = null;
		Certificate[] trustChain = null;

		// check if server key is in place
		System.out.println("SeedServer: using " + keyFilename);
		keyFile = new File(seedHome + File.separator + keyFilename);
		trustedFile = new File(seedHome + File.separator + trustedFilename);

		if(!(keyFile.exists())) // if the keyfile does not exist..
		{
			try
			// attempt to generate a certificate
			{
				CertAndKeyGen keyPair = new CertAndKeyGen("RSA", "SHA1WithRSA");
				System.out.println("SeedServer: generating 4096 bit rsa key ...");
				keyPair.generate(4096);
				System.out.println("SeedServer: key generation complete");
				PrivateKey privKey = keyPair.getPrivateKey();

				X500Name x500Name = new X500Name("CN=Unknown, OU=Unknown, O=Unknown, L=Unknown, ST=Unknown, C=Unknown");
				X509Certificate[] chain = new X509Certificate[1];
				chain[0] = keyPair.getSelfCertificate(x500Name, new Date(), 86400L * 365L * 3L); // 3
																									// years

				KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
				keyStore.load(null, keyPassword.toCharArray());
				keyStore.setKeyEntry("seedkey", privKey, keyPassword.toCharArray(), chain);
				keyStore.store(new FileOutputStream(keyFile), keyPassword.toCharArray());
			}
			catch(Exception e)
			{
				e.printStackTrace();
				return false;
			}
		}

		// print out server's fingerprint
		try
		{
			key = KeyStore.getInstance(KeyStore.getDefaultType());
			key.load(new FileInputStream(keyFile), keyPassword.toCharArray());

			System.out.println("identity key: ");

			Enumeration<String> aliases = key.aliases();
			while(aliases.hasMoreElements())
			{
				String alias = aliases.nextElement();
				keyCertificate = key.getCertificate(alias);
				if(keyCertificate instanceof X509Certificate)
				{
					System.out.print(alias + "\t");
					// for(byte b :
					// ((X509Certificate)keyCertificate).getSignature())
					for(byte b : MessageDigest.getInstance("SHA1").digest(keyCertificate.getEncoded()))
					{
						if(Integer.toString(b & 0xFF, 16).length() == 1) System.out.print("0");
						System.out.print(Integer.toString(b & 0xFF, 16) + " "); // maybe
						// force
						// to
						// hex?
					}
					System.out.println("\n");
				}
			}
		}
		catch(Exception e)
		{
			System.out.println("SeedServer: unable to obtain certificate from " + keyFilename);
			System.out.println(e);
			return false;
		}

		// print out trusted clients
		try
		{
			key = KeyStore.getInstance(KeyStore.getDefaultType());
			key.load(new FileInputStream(trustedFile.getPath()), keyPassword.toCharArray());

			System.out.println("trusted keys: ");

			Enumeration<String> aliases = key.aliases();
			while(aliases.hasMoreElements())
			{
				String alias = aliases.nextElement();
				keyCertificate = key.getCertificate(alias);
				if(keyCertificate instanceof X509Certificate)
				{
					System.out.print(alias + "\t");
					// for(byte b :
					// ((X509Certificate)keyCertificate).getSignature())
					for(byte b : MessageDigest.getInstance("SHA1").digest(keyCertificate.getEncoded()))
					{
						if(Integer.toString(b & 0xFF, 16).length() == 1) System.out.print("0");
						System.out.print(Integer.toString(b & 0xFF, 16) + " ");
					}
					System.out.println("");
				}
			}
		}
		catch(Exception e)
		{
			System.out.println("SeedServer: unable to obtain certificates from " + keyFilename);
			System.out.println(e);
			return false;
		}

		// set keystores
		System.setProperty("javax.net.ssl.keyStore", keyFile.getPath());
		System.setProperty("javax.net.ssl.keyStorePassword", keyPassword);
		System.setProperty("javax.net.ssl.trustStore", trustedFile.getPath());
		System.setProperty("javax.net.ssl.trustStorePassword", keyPassword);

		// create socket factories
		csf = new SslRMIClientSocketFactory();
		ssf = new SslRMIServerSocketFactory();

		return true;
	}

	static boolean loadSeeds()
	{
		// load all the seeds in the seeds directory
		File seedsDir = new File(SeedServer.seedHome.getPath() + File.separator + "seeds");
		if(seedsDir.exists() && seedsDir.isDirectory())
		{
			for(File seedFile : seedsDir.listFiles())
			{
				SeedServer.loadSeed(seedFile);
			}
		}

		return true;
	}

	public static String loadSeed(File newFile)
	{

		// add the file to the classpath
		URLClassLoader cl = null;
		try
		{
			System.out.println("SeedServer: loading " + newFile.toString());
			URL seedURL = newFile.toURI().toURL();
			cl = (URLClassLoader) ClassLoader.getSystemClassLoader();
			Method method = URLClassLoader.class.getDeclaredMethod("addURL", new Class[] { URL.class });
			method.setAccessible(true);
			method.invoke(cl, new Object[] { seedURL });
		}
		catch(Exception e)
		{
			newFile.delete();
			return newFile.getName() + " is not a loadable module";
		}

		// instantiate object
		String newSeedName = newFile.getName().split("\\.")[0]; // remove the
		// .jar from
		// filename
		Class newSeedClass = null;

		try
		{
			newSeedClass = cl.loadClass("javasec.seed.seeds." + newSeedName);
			newSeedClass.newInstance();

			// DataSeed.instance.insertRow("SEEDS",new Object[]{newSeedName});
		}
		catch(Exception e)
		{
			e.printStackTrace();
			System.out.println("SeedServer: " + newSeedName + " failed to load");
			// newFile.delete();
			return newFile.getName() + " is not a loadable module";
		}

		// ManagerSeed.instance.notifyClients("newseed",newSeedName);
		ManagerSeed.notifyClients(new SeedNotification("newseed", newSeedName));
		return null;
	}

	public static void detectBindAddress()
	{
		// for some reason rmi always identifies itself at localhost
		// we need to make sure that the server knows what its real
		// IP address is
		try
		{
			Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();

			System.out.println("enumerating interfaces looking for a good bind address");
			while(interfaces.hasMoreElements())
			{
				NetworkInterface networkInterface=interfaces.nextElement();
				
				// do not enumerate vmware interfaces
				// TODO  this will probabally only work in linux
				if(networkInterface.getDisplayName().contains("vmnet"))continue;
				
				Enumeration<InetAddress> addresses = networkInterface.getInetAddresses();
				while(addresses.hasMoreElements())
				{
					InetAddress inetAddress = addresses.nextElement();
					if(inetAddress.isLoopbackAddress())continue;
					if(!(inetAddress instanceof Inet4Address))continue;
					
					if(inetAddress.isSiteLocalAddress())
					{
						// set the internal IP temporarily
						bindAddress = inetAddress.getHostAddress();
						System.out.println("found internal IP at " + bindAddress);
					}
					else
					{
						// this is the external IP we were looking for
						bindAddress = inetAddress.getHostAddress();
						System.out.println("found external IP at " + bindAddress);
						return;
					}
				}
			}
			if(bindAddress==null)bindAddress="127.0.0.1";
		}
		catch(Exception e)
		{
			System.out.println("error: could not determine IP address");
			e.printStackTrace();
		}
	}

}
